import { Direction } from '../agStack/constants/direction';
import { _last } from '../agStack/utils/array';
import { _getActiveDomElement } from '../agStack/utils/document';
import { _observeResize } from '../agStack/utils/dom';
import { _findTabbableParent, _focusInto } from '../agStack/utils/focus';
import { BeanStub } from '../context/beanStub';
import type { FocusableContainer } from '../interfaces/iFocusableContainer';
import type { LayoutView } from '../styling/layoutFeature';
import { LayoutFeature } from '../styling/layoutFeature';
import { _isCellFocusSuppressed, _isHeaderFocusSuppressed } from '../utils/gridFocus';
import type { Component, ComponentSelector } from '../widgets/component';

export interface IGridComp extends LayoutView {
    setRtlClass(cssClass: string): void;
    destroyGridUi(): void;
    forceFocusOutOfContainer(up: boolean): void;
    getFocusableContainers(): FocusableContainer[];
    setCursor(value: string | null): void;
    setUserSelect(value: string | null): void;
}

export interface OptionalGridComponents {
    paginationSelector?: ComponentSelector<Component>;
    gridHeaderDropZonesSelector?: ComponentSelector<Component>;
    sideBarSelector?: ComponentSelector<Component>;
    statusBarSelector?: ComponentSelector<Component>;
    watermarkSelector?: ComponentSelector<Component>;
}

export class GridCtrl extends BeanStub {
    private view: IGridComp;
    private eGridHostDiv: HTMLElement;
    private eGui: HTMLElement;

    private readonly additionalFocusableContainers: Set<FocusableContainer> = new Set();

    public setComp(view: IGridComp, eGridDiv: HTMLElement, eGui: HTMLElement): void {
        this.view = view;
        this.eGridHostDiv = eGridDiv;
        this.eGui = eGui;

        this.eGui.setAttribute('grid-id', this.beans.context.getId());

        const { dragAndDrop, ctrlsSvc } = this.beans;

        dragAndDrop?.registerGridDropTarget(() => this.eGui, this);

        this.createManagedBean(new LayoutFeature(this.view));

        this.view.setRtlClass(this.gos.get('enableRtl') ? 'ag-rtl' : 'ag-ltr');

        const unsubscribeFromResize = _observeResize(this.beans, this.eGridHostDiv, this.onGridSizeChanged.bind(this));
        this.addDestroyFunc(() => unsubscribeFromResize());

        ctrlsSvc.register('gridCtrl', this);
    }

    public isDetailGrid(): boolean {
        const el = _findTabbableParent(this.getGui());

        return el?.getAttribute('row-id')?.startsWith('detail') || false;
    }

    public getOptionalSelectors(): OptionalGridComponents {
        const beans = this.beans;
        return {
            paginationSelector: beans.pagination?.getPaginationSelector(),
            gridHeaderDropZonesSelector: beans.registry?.getSelector('AG-GRID-HEADER-DROP-ZONES'),
            sideBarSelector: beans.sideBar?.getSelector(),
            statusBarSelector: beans.registry?.getSelector('AG-STATUS-BAR'),
            watermarkSelector: beans.licenseManager?.getWatermarkSelector(),
        };
    }

    private onGridSizeChanged(): void {
        this.eventSvc.dispatchEvent({
            type: 'gridSizeChanged',
            clientWidth: this.eGridHostDiv.clientWidth,
            clientHeight: this.eGridHostDiv.clientHeight,
        });
    }

    public destroyGridUi(): void {
        this.view.destroyGridUi();
    }

    public getGui(): HTMLElement {
        return this.eGui;
    }

    public setResizeCursor(direction: Direction | false): void {
        const { view } = this;

        if (direction === false) {
            view.setCursor(null);
        } else {
            const cursor = direction === Direction.Horizontal ? 'ew-resize' : 'ns-resize';
            view.setCursor(cursor);
        }
    }

    public disableUserSelect(on: boolean): void {
        this.view.setUserSelect(on ? 'none' : null);
    }

    public focusNextInnerContainer(backwards: boolean): boolean {
        const focusableContainers = this.getFocusableContainers();
        const { indexWithFocus, nextIndex } = this.getNextFocusableIndex(focusableContainers, backwards);

        if (nextIndex < 0 || nextIndex >= focusableContainers.length) {
            return false;
        }

        if (nextIndex === 0) {
            if (indexWithFocus > 0) {
                const { visibleCols, focusSvc } = this.beans;
                const allColumns = visibleCols.allCols;
                const lastColumn = _last(allColumns);
                if (focusSvc.focusGridView({ column: lastColumn, backwards: true })) {
                    return true;
                }
            }
            return false;
        }

        return this.focusContainer(focusableContainers[nextIndex], backwards);
    }

    public focusInnerElement(fromBottom?: boolean): boolean {
        const userCallbackFunction = this.gos.getCallback('focusGridInnerElement');
        if (userCallbackFunction?.({ fromBottom: !!fromBottom })) {
            return true;
        }

        const focusableContainers = this.getFocusableContainers();
        const { focusSvc, visibleCols } = this.beans;
        const allColumns = visibleCols.allCols;

        if (fromBottom) {
            if (focusableContainers.length > 1) {
                return this.focusContainer(_last(focusableContainers), fromBottom);
            }

            const lastColumn = _last(allColumns);
            if (focusSvc.focusGridView({ column: lastColumn, backwards: fromBottom })) {
                return true;
            }
        }

        if (this.gos.get('headerHeight') === 0 || _isHeaderFocusSuppressed(this.beans)) {
            if (focusSvc.focusGridView({ column: allColumns[0], backwards: fromBottom })) {
                return true;
            }

            for (let i = 1; i < focusableContainers.length; i++) {
                if (_focusInto(focusableContainers[i].getGui(), fromBottom)) {
                    return true;
                }
            }
            return false;
        }

        return focusSvc.focusFirstHeader();
    }

    public forceFocusOutOfContainer(up = false): void {
        this.view.forceFocusOutOfContainer(up);
    }

    public addFocusableContainer(container: FocusableContainer): void {
        this.additionalFocusableContainers.add(container);
    }

    public removeFocusableContainer(container: FocusableContainer): void {
        this.additionalFocusableContainers.delete(container);
    }

    public allowFocusForNextCoreContainer(up?: boolean): void {
        const coreContainers = this.view.getFocusableContainers();
        const { nextIndex } = this.getNextFocusableIndex(coreContainers, up);

        const comp = coreContainers[nextIndex];

        // if we got to this point, it means the user wants the browser's default focus behavior
        // we can no longer allow the browser's default behavior because scrollable divs are
        // considered focusable which causes the focus to become lost within the grid. So, here
        // we attempt to throw focus into the next container within the grid, or push focus out.
        if (comp) {
            this.focusContainer(comp);
        } else {
            this.forceFocusOutOfContainer(up);
        }
    }

    public isFocusable(): boolean {
        const beans = this.beans;
        return (
            !_isCellFocusSuppressed(beans) || !_isHeaderFocusSuppressed(beans) || !!beans.sideBar?.comp?.isDisplayed()
        );
    }

    private getNextFocusableIndex(
        focusableContainers: FocusableContainer[],
        backwards?: boolean
    ): {
        indexWithFocus: number;
        nextIndex: number;
    } {
        const activeEl = _getActiveDomElement(this.beans);
        const indexWithFocus = focusableContainers.findIndex((container) => container.getGui().contains(activeEl));
        const nextIndex = indexWithFocus + (backwards ? -1 : 1);
        return {
            indexWithFocus,
            nextIndex,
        };
    }

    private focusContainer(comp: FocusableContainer, up?: boolean): boolean {
        comp.setAllowFocus?.(true);
        const result = _focusInto(comp.getGui(), up, false, true);
        comp.setAllowFocus?.(false);
        return result;
    }

    private getFocusableContainers(): FocusableContainer[] {
        return [...this.view.getFocusableContainers(), ...this.additionalFocusableContainers];
    }

    public override destroy(): void {
        this.additionalFocusableContainers.clear();
        super.destroy();
    }
}
